NestJS で GraphQL サーバを実装する
インストール
code:sh
$ npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
Root module で GraphQLModule.forRoot をインポートする
autoSchemaFile に GraphQL Schema を生成するパスを指定する
code:app.module.ts
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver, // 使用するドライバ
playground: true, // プレイグラウンドを用いるか
autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // GraphQL Schema を生成するパス
}),
],
})
export class AppModule {}
オブジェクト型の定義
@ObjectType と @Field デコレータを用いる
code:src/task/models/task.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Task {
@Field(() => Int)
id: number;
@Field()
name: string;
@Field()
dueDate: string;
@Field()
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED';
@Field({ nullable: true })
description: string;
}
warning.icon 注意点
number は Float として変換されるので、Int にしたい場合は @Field で明示する必要がある
オプショナルにする場合は、@Field の引数に { nullable: true } を渡す必要がある
nullable: NULL 許容
description: フィールドの説明(コメント)
name: 上書き後のフィールド名
defaultValue: デフォルト値
npm run start:dev を実行すると、開発サーバが立ち上げるのと同時に、GraphQL Schema が生成される
code:src/schema.gql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type Task {
id: Float!
name: String!
dueDate: String!
status: String!
description: String
}
type Query {
}
Resolver
定義方法(ただし、nest g resolver コマンドで生成可能)
warning.icon ロジックは Service に記述する
1. class に @Resolver デコレータを付与する
2. 実現したい処理の内容をメソッドとして定義する
Query の場合は @Query、Mutation の場合は @Mutation デコレータを付与する
warning.icon Query は @nest/common ではなく、@nest/graphql から import すること
これらのデコレータには戻り値の型をアロー関数の形で渡す必要がある
code:ts
@Resolver()
export class TaskResolver {
constructor(private readonly taskService: TaskService) {}
@Query(() => Task, { nullable: 'items' }) getTasks(): Task[] {
...
}
@Mutation(() => Task)
createTask(
@Args('name') name: string,
@Args('dueDate') dueDate: string,
@Args('description', { nullable: true }) description: string,
): Task {
...
}
}
@Query / @Mutation
'items': 要素が空の場合は空配列になる([String]!)
itemsAndList: 要素が空の場合は null になる([String])
@Field と同様 name で名前を上書きすることも可能
@Args
リクエストで受け取る値はメソッドの引数
引数にはリクエスト時に使用するフィールド名を指定する
code:mutation.gql
mutation createTask($name:String!,$dueDate:String!,$description:String!) {
createTask(name:$name, dueDate:$dueDate, description:$description) {
id
name
dueDate
status
description
}
}
Pipe とバリデーション
Mutation に渡す値は、@InputType を用いて以下のように別クラスとして表現することも可能
code:createTask.input.ts
import { Field, InputType } from '@nestjs/graphql';
import { IsDateString, IsNotEmpty } from 'class-validator';
@InputType()
export class CreateTaskInput {
@Field()
@IsNotEmpty()
name: string;
@Field()
@IsDateString()
dueDate: string;
@Field({ nullable: true })
description?: string;
}
このクラスでは、DTO のようにバリデーションの責務を担うことが可能 ただし、バリデーションを走らせるには、NestJS の Pipe という機能を使う必要がある Controller や Resolver(Route Handler) がリクエストを受け取る前に、リクエストに対して処理を行う機能
https://docs.nestjs.com/assets/Pipe_1.png
手順
1. バリデーションを行うための組み込みの Pipe(ValidationPipe ) をグローバルにセットする
code:main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
// ...
}
2. Resolver と Service の引数で @InputType のインスタンスを受け取るようにする
code:ts
@Resolver()
export class TaskResolver {
// ...
@Mutation(() => Task)
createTask(@Args('createTaskInput') createTaskInput: CreateTaskInput): Task {
return this.taskService.createTask(createTaskInput);
}
}
code:ts
@Injectable()
export class TaskService {
// ...
createTask(createTaskInput: CreateTaskInput): Task {
const { name, dueDate, description } = createTaskInput;
return await this.prismaService.task.create({
data: { name, dueDate, description },
});
}
}
実際に不正なリクエストを送ると、以下のようなエラーが返ってくる
code:json
{
"errors": [
{
"message": "Bad Request Exception",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createTask"
],
"extensions": {
"code": "BAD_REQUEST",
"stacktrace": [
"..."
],
"originalError": {
"message": [
"name should not be empty",
"dueDate must be a valid ISO 8601 date string"
],
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}
Query に渡す値は@ArgsType を用いて以下のように別クラスとして表現することも可能
code:ts
@ArgsType()
export class GetUserArgs {
@Field()
@IsEmail()
email: string;
}
code:ts
@Query(() => UserModel, { nullable: true })
async getUser(@Args() getUserArgs: GetUserArgs): Promise<User> {
return await this.userService.getUser(getUserArgs.email);
}
@Mutation のときとは異なり、@Args に名前を表す文字列を渡さない
Playground
GraphQLModule.forRoot に { playground: true } を渡して開発サーバを立ち上げると、http://localhost:3000/graphql にアクセスしたときに Playground が表示される
https://scrapbox.io/files/66e03b58e9f56d001ce85280.png
参考